/*
 * Copyright (C) 2011 Google Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 * This code is not supported by Google
 *
 */

import java.io.File;
import java.io.FileInputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.PrivilegedAction;
import java.security.SecureRandom;
import java.security.cert.X509Certificate;
import java.util.Hashtable;
import java.util.Iterator;
import java.util.List;
import java.util.Properties;
import java.util.Vector;

import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.directory.Attribute;
import javax.naming.directory.Attributes;
import javax.naming.directory.DirContext;
import javax.naming.directory.InitialDirContext;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import javax.security.auth.Subject;
import javax.security.auth.login.LoginContext;
import javax.security.auth.login.LoginException;

import org.apache.http.Header;
import org.apache.http.HttpEntity;
import org.apache.http.HttpRequest;
import org.apache.http.HttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.client.params.HttpClientParams;
import org.apache.http.conn.ClientConnectionManager;
import org.apache.http.conn.scheme.PlainSocketFactory;
import org.apache.http.conn.scheme.Scheme;
import org.apache.http.conn.scheme.SchemeRegistry;
import org.apache.http.conn.ssl.SSLSocketFactory;
import org.apache.http.impl.client.ContentEncodingHttpClient;
import org.apache.http.impl.client.DefaultRedirectStrategy;
import org.apache.http.impl.conn.SingleClientConnManager;
import org.apache.http.params.BasicHttpParams;
import org.apache.http.params.HttpParams;
import org.apache.http.protocol.HttpContext;
import org.apache.http.util.EntityUtils;
import org.ietf.jgss.GSSContext;
import org.ietf.jgss.GSSCredential;
import org.ietf.jgss.GSSException;
import org.ietf.jgss.GSSManager;
import org.ietf.jgss.GSSName;
import org.ietf.jgss.Oid;
import org.apache.http.cookie.Cookie;
/*parts of this code is taken from
 * http://thejavamonkey.blogspot.com/2008/04/clientserver-hello-world-in-kerberos.html
 * http://krbdev.mit.edu/rt/Ticket/Display.html?user=guest&pass=guest&id=6402
 * http://www.ietf.org/rfc/rfc4178.txt
 * http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6815182
 * 
 * 
 * see:
 * http://code.google.com/p/gsa-kerberos-test-client/wiki/GSAKerbeorsTestApp
 * 
 * Requires SUN Java 1.7+
 * javac -classpath .:commons-codec-1.4.jar:commons-logging-1.1.1.jar:
 * 					httpclient-4.1.1.jar:httpclient-cache-4.1.1.jar:
 * 					httpcore-4.1.jar:httpmime-4.1.1.jar *.java
 * 
 * java -classpath .:commons-codec-1.4.jar:commons-logging-1.1.1.jar:
 * 					httpclient-4.1.1.jar:httpclient-cache-4.1.1.jar:
 * 					httpcore-4.1.jar:httpmime-4.1.1.jar Client 
 */

public class Client {
      private Oid krb5MechOid ;
      private Oid spnegoMechOid ;
	  private Subject subject;
	  private byte[] serviceTicket;
	  boolean kerberos_crawl = false;
	  boolean use_spnego=true;
	  private String username;
	  private String targetSPN;
	  private boolean debug_flag = false;
	     
  public static void main( String[] args) {
	  Client c = new Client(args);
  }
 
  public Client(String[] args) {
	  
      if (args.length>0)
    	  if (args[0].equalsIgnoreCase("crawl"))
    		  kerberos_crawl = true;

	    try {
	    	
	    	System.out.println("java version: " + System.getProperty("java.version"));
	    	System.out.println("java vendor: " + System.getProperty("java.vendor"));
	    	
	    	String jvm_version = System.getProperty("java.version").toString().split(":")[0];
	    	
	    	double jversion = Double.valueOf(jvm_version.substring(0,3));

	    	if (jversion<1.7)
	    	{
	    		//System.out.println("Requires SUN java 1.7+");
		    	//System.exit(0);
	    	}

	        Properties props = new Properties();
	        props.load( new FileInputStream( "system.properties"));
	        
	        debug_flag = new Boolean( props.getProperty("debug"));
	        System.setProperty( "sun.security.krb5.debug",  Boolean.toString(debug_flag));
	        System.setProperty( "sun.security.spnego.debug", Boolean.toString(debug_flag));

	        //System.setProperty("java.naming.provider.url", "dns://8.17.82.42/yourdomain.com");
	        
	        System.setProperty("java.security.krb5.conf",props.getProperty("kerberos.conf.file"));
	        System.setProperty( "java.security.auth.login.config", "jaas.conf");
	        //System.setProperty("sun.security.jgss.native","true");
	        //System.setProperty("sun.security.spnego.msinterop","true");
	        System.setProperty( "sun.security.spnego.debug", "true");
	        System.setProperty( "javax.security.auth.useSubjectCredsOnly", "false");           
	        
	        username = props.getProperty( "client.principal.name");
	        String password = props.getProperty( "client.password");
	        targetSPN = props.getProperty( "target.service.principal.name");

	        use_spnego = new Boolean( props.getProperty( "usespnego"));

	        //create the OID (for use later)
	        krb5MechOid    = new Oid("1.2.840.113554.1.2.2");
	        spnegoMechOid  = new Oid("1.3.6.1.5.5.2");
	        	      
  		    //check to see if the SPN we're trying to get a ticket for has some CNAME associated with it
  		    //if it does, windows issues service tickets for the 'A' name records by default.

  			  String shost = targetSPN;

           	  shost= targetSPN.toLowerCase();
  			  if (shost.startsWith("http/") || shost.startsWith("cifs/") )	{
  				  shost = shost.substring(5);
  			  }  
  			  else	  {
  				  System.out.println("Entered invalid SPN.  Must begin with HTTP/ or CIFS/");
  				  System.exit(-1);
  			  }
  			  this.checkSPNHostname(shost);  		    	  	

	        //login to the KDC using JAAS login module
	        this.subject = login(username, password);
	        
	        if (debug_flag)
	        	System.out.println(this.subject);
	        
	        //get the service ticket for targetSPN
	        this.serviceTicket= initiateSecurityContext( targetSPN );
	        	        
	        System.out.println("--------> Number of bytes in service ticket: [" + this.serviceTicket.length +"]");
	        System.out.println("If the number of bytes exceeds 16K, this may cause issues with the GSA");
	        	        
	        encodeAndWriteTicketToDisk( this.serviceTicket, "security.token");
	        System.out.println( "Service ticket encoded to disk successfully " );
	        
	        //should we try to crawl the target.url with this service ticket?
	       if (kerberos_crawl)
	       {	    	   
	      	    System.out.println("========== START CRAWL  =============");
		        // Create a trust manager that does not validate certificate chains

	      	SSLContext sslContext = SSLContext.getInstance("SSL");

	      	// set up a TrustManager that trusts everything
	      	sslContext.init(null, new TrustManager[] { new X509TrustManager() {
	      	            public X509Certificate[] getAcceptedIssuers() {
	      	                    return null;
	      	            }
	      	            public void checkClientTrusted(X509Certificate[] certs,
	      	                            String authType) {
	      	            }
	      	            public void checkServerTrusted(X509Certificate[] certs,
	      	                            String authType) {
	      	            }
	      	} }, new SecureRandom());
	      	
	      	Scheme httpScheme80 = new  Scheme("http", 80,  PlainSocketFactory.getSocketFactory());

	      	SSLSocketFactory sf = new SSLSocketFactory(sslContext,SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER);
	      	Scheme httpsScheme = new  Scheme("https", 443, sf);
	      	
	      	SchemeRegistry schemeRegistry = new SchemeRegistry();
	      	schemeRegistry.register(httpsScheme);
	      	schemeRegistry.register(httpScheme80);
	      		      	
	      	HttpParams params = new BasicHttpParams();
	      	HttpClientParams.setRedirecting(params, true);
	      	
	      	ClientConnectionManager cm = new SingleClientConnManager(schemeRegistry);

	      	ContentEncodingHttpClient httpclient = new ContentEncodingHttpClient(cm,params);

	        httpclient.setRedirectStrategy(new DefaultRedirectStrategy() {                
	            public HttpUriRequest getRedirect(HttpRequest request, HttpResponse response, HttpContext context)
	            {	
	            	if (debug_flag)
	            	for (Header h : response.getAllHeaders())
	            		System.out.println("<<< Redirect Header:  "+ h.getName() + " : " + h.getValue());
	            	
	            	Header dest_header = response.getFirstHeader("Location");
	            	HttpUriRequest rdr= new HttpGet(dest_header.getValue());
	            	System.out.println(">>> Redirecting to: " + dest_header.getValue());

	            	if (debug_flag)
	            	for (Header h : request.getAllHeaders())
	            		System.out.println(">>> Redirect Header:  "+ h.getName() + " : " + h.getValue());

                    return rdr;
	            }
	        });
	        
	      	HttpGet httpget = new HttpGet(props.getProperty("target.url"));

  	        Properties hprops = new Properties();
  	        
	        hprops.load( new FileInputStream( "httpheaders.txt"));
	        Iterator hpitr = hprops.keySet().iterator();
            while (hpitr.hasNext()) {
            	String key = hpitr.next().toString();
            	String value = hprops.getProperty(key);
            	httpget.addHeader(key, value); 
            }	
  			
	      	byte[] encodedBytes  = org.apache.commons.codec.binary.Base64.encodeBase64(this.serviceTicket);
  			String encoded =   new String(encodedBytes); 
  			
  			httpget.addHeader("Authorization", "Negotiate " + encoded);
  			
  			if (debug_flag)
  			for (Header h : httpget.getAllHeaders())
  				System.out.println(">>> Header:  "+ h.getName() + " : " + h.getValue());
  			
  			HttpResponse response = httpclient.execute(httpget);

  		    HttpEntity entity = response.getEntity(); 
  		    String content = EntityUtils.toString(entity); 
      	    
      	    System.out.println(" ");
      	    System.out.println(response.getStatusLine());
      	    
      	    if (debug_flag)
  			for (Header h : response.getAllHeaders())
  				System.out.println("<<< Header:  "+ h.getName() + " : " + h.getValue());
  			
      	    

      	    if (debug_flag)
            for (Cookie cookie : httpclient.getCookieStore().getCookies()) {
          	       System.out.println("<<< Cookies: ");            
                   System.out.println(cookie.getName() + " " + cookie.getValue());
            }
            
            System.out.println(" ");
            System.out.println("<<< Response: ");
            
            System.out.println(" ");
      	    System.out.println(content);

 		    System.out.println("========== END CRAWL  =============");
	       }	      
	      }
	      catch ( LoginException e) {
	        System.out.println("Unable to login to the KDC using username/password");

	        if (e.getMessage().equalsIgnoreCase("Connection refused: connect"))
	        	System.out.println("-----------> Check KDC hostname/IP in config file");
	        	
	        if (e.getMessage().equalsIgnoreCase("KDC has no support for encryption type (14)"))
	        	System.out.println("----------->  Enable DES encryption for this user");

	        if (e.getMessage().equalsIgnoreCase("Clients credentials have been revoked (18)"))
	        	System.out.println("----------->  User account disabled/expired");
	        
	        if (e.getMessage().equalsIgnoreCase("Pre-authentication information was invalid (24)"))
	        	System.out.println("----------->  Possible invalid username/password");

	        System.exit( -1);
	      }
	      catch ( GSSException e) {
	        e.printStackTrace();
	        System.err.println( "There was an error during the security context initiation");
	        System.exit( -1);
	      }
	      catch ( IOException e) {
	        e.printStackTrace();
	        System.err.println( "There was an IO error");
	        System.exit( -1);
	      } catch (NoSuchAlgorithmException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} catch (KeyManagementException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		} 
  }

  // Authenticate against the KDC using JAAS.
  private Subject login( String username, String password) throws LoginException {
    LoginContext loginCtx = null;
    System.out.println("========== START LOGIN =============");
    System.out.println("Attempting client username/password based login with");
    System.out.println("module: com.sun.security.auth.module.Krb5LoginModule");
    loginCtx = new LoginContext( "Client", new  LoginCallbackHandler(username, password));
    loginCtx.login();  
    System.out.println("========== END LOGIN =============");
    return  loginCtx.getSubject();
  }
 
  // Begin the initiation of a security context with the target service.
  private byte[]  initiateSecurityContext( String servicePrincipalName)
      throws GSSException {
	
			System.out.println("========== START ACQUIRE SERVICE TKT =============");
		    System.out.println("Attempting to acquire ServiceTicket for " +  servicePrincipalName);

		    byte svc_tkt[] = Subject.doAs(subject, new PrivilegedAction<byte[]>() 
		    {
		        public byte[] run() 
		        {
		            try {
		            	GSSManager manager = GSSManager.getInstance();
	
		            	//gssNamespn isn't currently used because we 
		            	//only need just the kerb5 creds to create the context.
		               GSSName  gssNamespn = manager.createName(username, GSSName.NT_USER_NAME, spnegoMechOid);  
		              
		               GSSName  gssNamekrb = manager.createName(username, GSSName.NT_USER_NAME, krb5MechOid);  
		               
		               //create a kerberos5 credentail
		               GSSCredential gssCred = manager.createCredential(
		                		gssNamekrb, GSSCredential.DEFAULT_LIFETIME, krb5MechOid, GSSCredential.INITIATE_ONLY);		            
		                		
		               //GSSCredential gssCred = manager.createCredential(
		               //		   gssNamespn, GSSCredential.DEFAULT_LIFETIME, spnegoMechOid, GSSCredential.INITIATE_ONLY);			               
		               //gssCred.add(gssNamekrb,GSSCredential.DEFAULT_LIFETIME, 
		               //		   GSSCredential.DEFAULT_LIFETIME, krb5MechOid,GSSCredential.INITIATE_ONLY);		               

		                
		                Oid [] mechs = gssCred.getMechs();

		                //list out themechs in case we wanted to see which ones are used (we already know)
		                if (mechs != null) 
		                        for (int i = 0; i < mechs.length; i++)
		                                System.out.println("Credential Mechanism: " + mechs[i].toString());
 
		    		    GSSName serverName = manager.createName(targetSPN, GSSName.NT_USER_NAME);
		    		    
		    		    //JDK 1.6 as a bug in its SPNEGO so we can't use it
		    		    //http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6815182
		    		    GSSContext context = null;
		    		    
		    		    if (use_spnego) {
		    		    	System.out.println("Attempting to use SPNEGO Mech");
		    		    	context = manager.createContext(serverName, spnegoMechOid ,gssCred,GSSContext.DEFAULT_LIFETIME);		    		    	
		    		    }
		    		    else {
		    		    	System.out.println("Attempting to use krb5 Mech");
		    		    	context = manager.createContext(serverName, krb5MechOid ,gssCred,GSSContext.DEFAULT_LIFETIME);
		    		    }
		    		    
		    		    //set the context flags....we're always setting the delegation flag
		    		    //to true here
		    		    byte[] token = new byte[0];
		    		    context.requestMutualAuth(true);
		    		    context.requestCredDeleg(true);   
		    		    context.requestReplayDet(false);
		    		    context.requestSequenceDet(false);
		    		    context.requestConf(false);
		    		    context.requestInteg(false); 
		    		    context.requestAnonymity(false); 
		    		    
		    		    token =context.initSecContext( token, 0, token.length);
		    		    
		    		    System.out.println("Acquired ticket: ");
		    		    System.out.println("Source:---> " + context.getSrcName());
		    		    System.out.println("Target:---> " + context.getTargName());
		    		    System.out.println("Context Mechanism: " +  context.getMech().toString()); 
		    		    
		    		    System.out.println("========== END  ACQUIRE SERVICE TKT =============");

		                return token;
		            } catch (GSSException gsse) {
		            	if (gsse.getMajor() == GSSException.NO_CRED) {
		            			System.out.println("Could not acquire Service ticket for SPN " );
		            			System.out.println("Potential reasons: " );		            					            			
		            			System.out.println("1. The SPN doesn't exist in ActiveDirectory" );		            			
		            			System.out.println("2. The SPNs host is defined as CNAME record in DNS" );		            			
		            			System.out.println("GSSException " +gsse);
		            	}
		            	System.exit(-1);
		            } catch (Exception e) {
		            	System.out.println("General Exception  " +e);
		            	System.exit(-1);
		            }
		            return null;
		        }
		    });
		    return svc_tkt;	    
    }

  // Base64 encode the raw ticket and write it to the given file.
  private static void encodeAndWriteTicketToDisk( byte[] ticket, String filepath)
      throws IOException {
    byte[] encodedBytes  = org.apache.commons.codec.binary.Base64.encodeBase64(ticket);
    FileWriter writer = new FileWriter( new File( filepath));
    String svc_tkt = new String(encodedBytes);
    System.out.println("Base64Encoded service ticket (HTTP Authorization Header): " + svc_tkt);
    writer.write(svc_tkt);
    writer.close();
  }
  
  private void checkSPNHostname(String hostname)
  {
      List result = new Vector();
      try {
    	  System.out.println("Trying to verify if  [" + hostname + "] has a CNAME");
          Hashtable env = new Hashtable();
          env.put("java.naming.factory.initial", "com.sun.jndi.dns.DnsContextFactory");
          DirContext ictx = new InitialDirContext(env);
          Attributes attrs = ictx.getAttributes(hostname, new String[] { "CNAME" });
          Attribute attr_cn = attrs.get("CNAME");
        
          if (attr_cn == null)
          {
              // CNAME doesn't exist, returning
        	  System.out.println("[" + hostname + "]" + "  does not have a CNAME entry. ");
        	  return;
          }
          NamingEnumeration attrEnum = attr_cn.getAll();
          while (attrEnum.hasMoreElements())
          {
        	  System.out.println("[" + hostname + "]" + " has a CNAME entry.  Verify if the SPN matches " + attrEnum.next());
          }
      } catch (NamingException e) {
          e.printStackTrace();
      } catch (NullPointerException e) {
    	  return;
      }
  
  }
  
  
 
}

